/*******************************************************************************
 * Copyright (c) 2011 Benjamin King.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Benjamin King - initial API and implementation
 ******************************************************************************/
package net.blackhacker.supermarketapi;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;


/**
 * SuperMarketAPI
 * 
 * @author Ben King (ben@benking.org)
 *
 */
public class SuperMarketAPI {
	/**
	 * Base URL to the SupermarketAPI
	 */
	final public static String API_URL = "http://www.SupermarketAPI.com/api.asmx/";

	/**
	 * Initial size of content buffer
	 */
	final public static int CONTENT_BUFFER_SIZE = 1024 * 16;
	
	private static HttpClient httpClient=null;
	private static String apiKey = null;
	private static Document allUSStates = null;
	
	private String errorMsg = "";
	private byte[] content = null;
	private Document result = null;

	private SuperMarketAPI () { };
	
	/**
	 * Factory Method to create new instance of SuperMarketAPI
	 * 
	 * @param httpClient
	 * @param apiKey
	 * @return SuperMarketAPI or null if httpClient or apiKey is null
	 */
	static public SuperMarketAPI newInstance(HttpClient httpClient, String apiKey) {
		if (SuperMarketAPI.httpClient==null)
			SuperMarketAPI.httpClient = httpClient;
		
		if (SuperMarketAPI.apiKey==null)
			SuperMarketAPI.apiKey = apiKey;
		
		return newInstance();
	}
	
	/**
	 * Factory Method to create new instance of SuperMarketAPI
	 * 
	 * @return SuperMarketAPI or null if httpClient or apiKey is null
	 */
	static public SuperMarketAPI newInstance() {
		
		if (httpClient==null)
			return null;
		
		if (apiKey==null)
			return null;
		
		return new SuperMarketAPI();
	}
	
	/**
	 * sets the httpClient class variable
	 * 
	 * @param httpClient
	 */
	static public void setHttpClient(HttpClient httpClient) {
		SuperMarketAPI.httpClient = httpClient;
	}
	
	/**
	 * Sets the apiKey class variable
	 * 
	 * @param apiKey
	 */
	static public void setApiKey(String apiKey){
		SuperMarketAPI.apiKey = apiKey;
	}
	
	/**
	 * creates a new content buffer is one doesn't exist yet, otherwise uses existing one.
	 * cleans buffer with newlines so the DOM parser won't choke
	 * 
	 * @return the content buffer in the form of an array bytes
	 */
	public byte[] newContentBuffer() {
		if (content==null)
			content = new byte[CONTENT_BUFFER_SIZE];
		
		 Arrays.fill(content, (byte)'\n');
		 return content;
	}

	/**
	 * getter for the contentBuffer
	 * 
	 * @return the content buffer in the form of an array bytes
	 */
	public byte[] getContentBuffer() {
		if(content==null)
			return newContentBuffer();
		
		return content;
	}

	/**
	 * increases the size of contentBuffer by CONTENT_BUFFER_SIZE
	 * 
	 * @return the content buffer in the form of an array bytes
	 */
	public byte[] increaseContentBuffer() {
		byte[] newBuffer = new byte[content.length + CONTENT_BUFFER_SIZE];
		Arrays.fill(newBuffer, (byte)'\n');
		
		for(int x=0 ; x < content.length ; x++)
			newBuffer[x] = content[x];
		
		return content = newBuffer;
	}

	/**
	 * getter for result
	 * @return DOM object of last successful API request
	 */
	public Document getResult() {
		return result;
	}
	
	/**
	 * getter for errorMsg
	 * 
	 * @return errorMsg
	 */
	public String getErrorMsg() {
		return errorMsg;
	}
	
	/**
	 * Performs SearchByProductName command 
	 * 
	 * @param itemName name of Item to search for
	 * @return DOM object containing results
	 */
	public Document searchByProductName(String itemName) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("ItemName", itemName));

        if (doCommand("SearchByProductName", nvps))
        	return getResult();
        
		return null;
	}
	/**
	 * Performs SearchByItemID command
	 * 
	 * @param itemId unique identifier 
	 * @return DOM object containing results
	 */
	public Document searchByItemID(String itemId) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("ItemID", itemId));

        if (doCommand("SearchByItemID", nvps))
        	return getResult();
        
		return null;
	}

	/**
	 * Returns all supported states
	 * @return 
	 */
	public Document allUSStates() {
		
		if (allUSStates!=null)
			return allUSStates;

        if (doCommand("AllUSStates", null))
        	return allUSStates = getResult();
        
		return null;
	}
	
	/**
	 * returns Cities in a given state
	 * 
	 * @param state
	 * @return
	 */
	public Document citiesByState(String state){
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("SelectedState", state));
        
		if(doCommand("CitiesByState", nvps))
			return getResult();
		
		return null;
	}

	/**
	 * Returns groceries
	 * 
	 * @param searchText
	 * @return
	 */
	public Document getGroceries(String searchText) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("SearchText", searchText));
        
        if (doCommand("GetGroceries", nvps))
        	return getResult();
        
        return null;
	}

	/**
	 * Finds Stores that match a given name
	 * 
	 * @param storeName
	 * @return
	 */
	public Document returnStoresByName(String storeName) {
		List <NameValuePair> nvps = new LinkedList<NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("StoreName", storeName));
        
        if (doCommand("ReturnStoresByName", nvps))
        	return getResult();
        
        return null;
	}
	
	/**
	 * Finds items i a given store
	 * 
	 * @param storeId
	 * @param itemName
	 * @return
	 */
	public Document searchForItem(String storeId, String itemName) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("StoreId", storeId));
        nvps.add(new BasicNameValuePair("ItemName", itemName));
        
        if (doCommand("SearchForItem", nvps))
        	return getResult();
        
        return null;
	}

	/**
	 * 
	 * @param selectedCity
	 * @param selectedState
	 * @return
	 */
	public Document storesByCityState(String selectedCity, String selectedState) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("SelectedCity", selectedCity));
        nvps.add(new BasicNameValuePair("SelectedState", selectedState));
        
        if (doCommand("StoresByCityState", nvps))
        	return getResult();
        
        return null;
	}

	/**
	 * Finds Stores in a given zipcode
	 * 
	 * @param zipCode
	 * @return
	 */
	public Document storesByZip(String zipCode) {
		List <NameValuePair> nvps = new LinkedList <NameValuePair>();
        nvps.add(new BasicNameValuePair("APIKEY", apiKey));
        nvps.add(new BasicNameValuePair("ZipCode", zipCode));
        
        if (doCommand("StoresByZip", nvps))
        	return getResult();
        
        return null;
	}	
	
	/**
	 * performs HTTP POST request to API_URL. The response to a successful
	 * request is stored in contents variable
	 * 
	 * @param command command to be performed
	 * @param nvps query parameters
	 * @return true when request is successful, false when there is a problem.
	 */
	public boolean doCommand(String command, List <NameValuePair> nvps) {

		//clear errorMsg for each API call
		errorMsg = "";
		
		try {
			if (SuperMarketAPI.httpClient == null) {
				errorMsg = "httpClient is null";
				return false;
			}
				
			HttpPost post = new HttpPost(API_URL + command);
			
			if (nvps != null)
				post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
			
			HttpResponse response =  SuperMarketAPI.httpClient.execute(post);		
			int sc = response.getStatusLine().getStatusCode();
			
			if (sc != HttpStatus.SC_OK) {
				errorMsg = response.getStatusLine().getReasonPhrase();
				return false;
			}
			
			HttpEntity entity = response.getEntity();
			
			byte[] content = newContentBuffer();
			int offset = 0;
			int r = 0;
			
			InputStream in  = new BufferedInputStream(entity.getContent(),content.length);
			
			while((r = in.read(content, offset, content.length - offset)) > -1){
				if (r ==(content.length - offset)) {
					//content buffer is full  build a bigger buffer
					content = increaseContentBuffer();
				}
				
				offset += r;
			}
		
			in.close();
			
			DocumentBuilder domBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			result  = domBuilder.parse(new InputSource(new ByteArrayInputStream(content)));
			
			return true;
		} catch (Exception e) {
			errorMsg = e.getMessage();
		}

		return false;
	}	
}
